#!/usr/bin/python -u
# Copyright (c) 2010-2012 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import signal
import socket

from errno import EEXIST, ENOENT
from hashlib import md5
from optparse import OptionParser, SUPPRESS_HELP
from os import environ, listdir, makedirs, utime, _exit as os_exit
from os.path import basename, dirname, getmtime, getsize, isdir, join
from Queue import Empty, Queue
from random import shuffle
from sys import argv, exc_info, exit, stderr, stdout
from threading import current_thread, enumerate as threading_enumerate, Thread
from time import sleep, time
from traceback import format_exception
from urllib import quote, unquote

from swiftclient import Connection, ClientException, HTTPException


def get_conn(options):
    """
    Return a connection building it from the options.
    """
    return Connection(options.auth,
                      options.user,
                      options.key,
                      auth_version=options.auth_version,
                      os_options=options.os_options,
                      snet=options.snet)


def mkdirs(path):
    try:
        makedirs(path)
    except OSError, err:
        if err.errno != EEXIST:
            raise


def put_errors_from_threads(threads, error_queue):
    """
    Places any errors from the threads into error_queue.
    :param threads: A list of QueueFunctionThread instances.
    :param error_queue: A queue to put error strings into.
    :returns: True if any errors were found.
    """
    was_error = False
    for thread in threads:
        for info in thread.exc_infos:
            was_error = True
            if isinstance(info[1], ClientException):
                error_queue.put(str(info[1]))
            else:
                error_queue.put(''.join(format_exception(*info)))
    return was_error


def attempt_graceful_exit(signum, frame):
    """
    Try to gracefully shut down. Sets abort=True on all non-main threads.

    More importantly, installs the immediate_exit handler on the
    signal that triggered this handler. If this function is installed
    as a signal handler for SIGINT, then pressing Ctrl-C once will
    cause this program to finish operations in progress, then exit.
    Pressing it again will cause an immediate exit; no cleanup
    handlers will get called.
    """
    stderr.write("Attempting graceful exit. "
                 "Press Ctrl-C again to exit immediately.\n")
    main_thread = current_thread()
    for thread in [t for t in threading_enumerate() if t is not main_thread]:
        thread.abort = True
    signal.signal(signum, immediate_exit)


def immediate_exit(signum, frame):
    os_exit(2)


class QueueFunctionThread(Thread):

    def __init__(self, queue, func, *args, **kwargs):
        """ Calls func for each item in queue; func is called with a queued
            item as the first arg followed by *args and **kwargs. Use the abort
            attribute to have the thread empty the queue (without processing)
            and exit. """
        Thread.__init__(self)
        self.abort = False
        self.queue = queue
        self.func = func
        self.args = args
        self.kwargs = kwargs
        self.exc_infos = []

    def run(self):
        while True:
            try:
                item = self.queue.get_nowait()
            except Empty:
                if self.abort:
                    break
                sleep(0.01)
            else:
                try:
                    if not self.abort:
                        self.func(item, *self.args, **self.kwargs)
                except Exception:
                    self.exc_infos.append(exc_info())
                finally:
                    self.queue.task_done()

st_delete_help = '''
delete [options] --all OR delete container [options] [object] [object] ...
    Deletes everything in the account (with --all), or everything in a
    container, or a list of objects depending on the args given. Segments of
    manifest objects will be deleted as well, unless you specify the
    --leave-segments option.'''.strip('\n')


def st_delete(parser, args, print_queue, error_queue):
    parser.add_option('-a', '--all', action='store_true', dest='yes_all',
        default=False, help='Indicates that you really want to delete '
        'everything in the account')
    parser.add_option('', '--leave-segments', action='store_true',
        dest='leave_segments', default=False, help='Indicates that you want '
        'the segments of manifest objects left alone')
    parser.add_option('', '--object-threads', type=int,
                      default=10, help='Number of threads to use for '
                      'deleting objects')
    parser.add_option('', '--container-threads', type=int,
                      default=10, help='Number of threads to use for '
                      'deleting containers')
    (options, args) = parse_args(parser, args)
    args = args[1:]
    if (not args and not options.yes_all) or (args and options.yes_all):
        error_queue.put('Usage: %s [options] %s' %
                        (basename(argv[0]), st_delete_help))
        return

    def _delete_segment((container, obj), conn):
        conn.delete_object(container, obj)
        if options.verbose:
            if conn.attempts > 2:
                print_queue.put('%s/%s [after %d attempts]' %
                                (container, obj, conn.attempts))
            else:
                print_queue.put('%s/%s' % (container, obj))

    object_queue = Queue(10000)

    def _delete_object((container, obj), conn):
        try:
            old_manifest = None
            if not options.leave_segments:
                try:
                    old_manifest = conn.head_object(container, obj).get(
                        'x-object-manifest')
                except ClientException, err:
                    if err.http_status != 404:
                        raise
            conn.delete_object(container, obj)
            if old_manifest:
                segment_queue = Queue(10000)
                scontainer, sprefix = old_manifest.split('/', 1)
                scontainer = unquote(scontainer)
                sprefix = unquote(sprefix)
                for delobj in conn.get_container(scontainer,
                                                 prefix=sprefix)[1]:
                    segment_queue.put((scontainer, delobj['name']))
                if not segment_queue.empty():
                    segment_threads = [QueueFunctionThread(segment_queue,
                        _delete_segment, create_connection()) for _junk in
                        xrange(options.object_threads)]
                    for thread in segment_threads:
                        thread.start()
                    while not segment_queue.empty():
                        sleep(0.01)
                    for thread in segment_threads:
                        thread.abort = True
                        while thread.isAlive():
                            thread.join(0.01)
                    put_errors_from_threads(segment_threads, error_queue)
            if options.verbose:
                path = options.yes_all and join(container, obj) or obj
                if path[:1] in ('/', '\\'):
                    path = path[1:]
                if conn.attempts > 1:
                    print_queue.put('%s [after %d attempts]' %
                                    (path, conn.attempts))
                else:
                    print_queue.put(path)
        except ClientException, err:
            if err.http_status != 404:
                raise
            error_queue.put('Object %s not found' %
                            repr('%s/%s' % (container, obj)))

    container_queue = Queue(10000)

    def _delete_container(container, conn):
        try:
            marker = ''
            had_objects = False
            while True:
                objects = [o['name'] for o in
                           conn.get_container(container, marker=marker)[1]]
                if not objects:
                    break
                had_objects = True
                for obj in objects:
                    object_queue.put((container, obj))
                marker = objects[-1]
            if had_objects:
                # By using join() instead of empty() we should avoid most
                # occurrences of 409 below.
                object_queue.join()
            attempts = 1
            while True:
                try:
                    conn.delete_container(container)
                    break
                except ClientException, err:
                    if err.http_status != 409:
                        raise
                    if attempts > 10:
                        raise
                    attempts += 1
                    sleep(1)
        except ClientException, err:
            if err.http_status != 404:
                raise
            error_queue.put('Container %s not found' % repr(container))

    create_connection = lambda: get_conn(options)
    object_threads = \
        [QueueFunctionThread(object_queue, _delete_object, create_connection())
         for _junk in xrange(options.object_threads)]
    for thread in object_threads:
        thread.start()
    container_threads = \
        [QueueFunctionThread(container_queue, _delete_container,
                             create_connection())
         for _junk in xrange(options.container_threads)]
    for thread in container_threads:
        thread.start()
    if not args:
        conn = create_connection()
        try:
            marker = ''
            while True:
                containers = \
                    [c['name'] for c in conn.get_account(marker=marker)[1]]
                if not containers:
                    break
                for container in containers:
                    container_queue.put(container)
                marker = containers[-1]
            while not container_queue.empty():
                sleep(0.01)
            while not object_queue.empty():
                sleep(0.01)
        except ClientException, err:
            if err.http_status != 404:
                raise
            error_queue.put('Account not found')
    elif len(args) == 1:
        if '/' in args[0]:
            print >> stderr, 'WARNING: / in container name; you might have ' \
                             'meant %r instead of %r.' % \
                             (args[0].replace('/', ' ', 1), args[0])
        conn = create_connection()
        _delete_container(args[0], conn)
    else:
        for obj in args[1:]:
            object_queue.put((args[0], obj))
    while not container_queue.empty():
        sleep(0.01)
    for thread in container_threads:
        thread.abort = True
        while thread.isAlive():
            thread.join(0.01)
    put_errors_from_threads(container_threads, error_queue)
    while not object_queue.empty():
        sleep(0.01)
    for thread in object_threads:
        thread.abort = True
        while thread.isAlive():
            thread.join(0.01)
    put_errors_from_threads(object_threads, error_queue)


st_download_help = '''
download --all OR download container [options] [object] [object] ...
    Downloads everything in the account (with --all), or everything in a
    container, or a list of objects depending on the args given. For a single
    object download, you may use the -o [--output] <filename> option to
    redirect the output to a specific file or if "-" then just redirect to
    stdout.'''.strip('\n')


def st_download(parser, args, print_queue, error_queue):
    parser.add_option('-a', '--all', action='store_true', dest='yes_all',
        default=False, help='Indicates that you really want to download '
        'everything in the account')
    parser.add_option('-m', '--marker', dest='marker',
        default='', help='Marker to use when starting a container or '
        'account download')
    parser.add_option('-o', '--output', dest='out_file', help='For a single '
        'file download, stream the output to an alternate location ')
    parser.add_option('', '--object-threads', type=int,
                      default=10, help='Number of threads to use for '
                      'downloading objects')
    parser.add_option('', '--container-threads', type=int,
                      default=10, help='Number of threads to use for '
                      'listing containers')
    parser.add_option('', '--no-download', action='store_true',
                      default=False, help="Perform download(s), but don't "
                      "actually write anything to disk")
    (options, args) = parse_args(parser, args)
    args = args[1:]
    if options.out_file == '-':
        options.verbose = 0
    if options.out_file and len(args) != 2:
        exit('-o option only allowed for single file downloads')
    if (not args and not options.yes_all) or (args and options.yes_all):
        error_queue.put('Usage: %s [options] %s' %
                        (basename(argv[0]), st_download_help))
        return

    object_queue = Queue(10000)

    def _download_object(queue_arg, conn):
        # conn object already exists here
        # queue_arg could be "cloud:container object" syntax
        if len(queue_arg) == 2:
            container, obj = queue_arg
            out_file = None
        elif len(queue_arg) == 3:
            container, obj, out_file = queue_arg
        else:
            raise Exception("Invalid queue_arg length of %s" % len(queue_arg))
        #
        # If "container" has :, then split into target_cloud and container
        #
        split_args = container.split(':')
        if len(split_args) == 1:
            conn.target_cloud = 'LocalCloud'
            # 'container' is just the container name
        elif len(split_args) == 2:
            conn.target_cloud = split_args[0]
            container = split_args[1]
        else:
            error_queue.put('Malformed cloudname:containername %s' % container )
            return

        try:
            start_time = time()
            # Actual VO login would be done (if appro) when conn.get_object() called
            # conn.target_cloud is set to either LocalCloud or target cloud name
            # Keystone does VO login but returns just URL and AUTH_TOKEN for target_cloud
            #   for use by conn.get_object()
            headers, body = \
                conn.get_object(container, obj, resp_chunk_size=65536)
            header_receipt = time()
            content_type = headers.get('content-type')
            if 'content-length' in headers:
                content_length = int(headers.get('content-length'))
            else:
                content_length = None
            etag = headers.get('etag')
            path = options.yes_all and join(container, obj) or obj
            if path[:1] in ('/', '\\'):
                path = path[1:]
            md5sum = None
            make_dir = not options.no_download and out_file != "-"
            if content_type.split(';', 1)[0] == 'text/directory':
                if make_dir and not isdir(path):
                    mkdirs(path)
                read_length = 0
                if 'x-object-manifest' not in headers:
                    md5sum = md5()
                for chunk in body:
                    read_length += len(chunk)
                    if md5sum:
                        md5sum.update(chunk)
            else:
                dirpath = dirname(path)
                if make_dir and dirpath and not isdir(dirpath):
                    mkdirs(dirpath)
                if not options.no_download:
                    if out_file == "-":
                        fp = stdout
                    elif out_file:
                        fp = open(out_file, 'wb')
                    else:
                        fp = open(path, 'wb')
                read_length = 0
                if 'x-object-manifest' not in headers:
                    md5sum = md5()
                for chunk in body:
                    if not options.no_download:
                        fp.write(chunk)
                    read_length += len(chunk)
                    if md5sum:
                        md5sum.update(chunk)
                if not options.no_download:
                    fp.close()
            if md5sum and md5sum.hexdigest() != etag:
                error_queue.put('%s: md5sum != etag, %s != %s' %
                                (path, md5sum.hexdigest(), etag))
            if content_length is not None and read_length != content_length:
                error_queue.put('%s: read_length != content_length, %d != %d' %
                                (path, read_length, content_length))
            if 'x-object-meta-mtime' in headers and not options.out_file \
                    and not options.no_download:

                mtime = float(headers['x-object-meta-mtime'])
                utime(path, (mtime, mtime))
            if options.verbose:
                finish_time = time()
                time_str = 'headers %.3fs, total %.3fs, %.3fs MB/s' % (
                    header_receipt - start_time, finish_time - start_time,
                    float(read_length) / (finish_time - start_time) / 1000000)
                if conn.attempts > 1:
                    print_queue.put('%s [%s after %d attempts]' %
                                    (path, time_str, conn.attempts))
                else:
                    print_queue.put('%s [%s]' % (path, time_str))
        except ClientException, err:
            if err.http_status != 404:
                raise
            error_queue.put('Object %s not found' %
                            repr('%s/%s' % (container, obj)))

    container_queue = Queue(10000)

    def _download_container(container, conn):
        try:
            marker = options.marker
            while True:
                objects = [o['name'] for o in
                           conn.get_container(container, marker=marker)[1]]
                if not objects:
                    break
                marker = objects[-1]
                shuffle(objects)
                for obj in objects:
                    object_queue.put((container, obj))
        except ClientException, err:
            if err.http_status != 404:
                raise
            error_queue.put('Container %s not found' % repr(container))

    # Local routines end here and the main body of st_download begins

    create_connection = lambda: get_conn(options)

    # These threads are pull arguments off of queue objects
    # The object threads are probably using the object queue
    # create_connection() is called and THEN initial conn object is put on queue
    # get_conn() just creates the conn object, actual login is done on subsequent call
    #  e.g., conn.get_account() -- here it will be conn.get_object()

    object_threads = [QueueFunctionThread(object_queue, _download_object, create_connection())
                      for _junk in xrange(options.object_threads)]
    for thread in object_threads:
        thread.start()
    container_threads = [QueueFunctionThread(container_queue, _download_container, create_connection())
                         for _junk in xrange(options.container_threads)]
    for thread in container_threads:
        thread.start()

    if not args:
        conn = create_connection()
        try:
            marker = options.marker
            while True:
                containers = [c['name']
                              for c in conn.get_account(marker=marker)[1]]
                if not containers:
                    break
                marker = containers[-1]
                shuffle(containers)
                for container in containers:
                    container_queue.put(container)
        except ClientException, err:
            if err.http_status != 404:
                raise
            error_queue.put('Account not found')
    elif len(args) == 1:
        if '/' in args[0]:
            print >> stderr, 'WARNING: / in container name; you might have ' \
                             'meant %r instead of %r.' % \
                             (args[0].replace('/', ' ', 1), args[0])
        _download_container(args[0], create_connection())
    else:
        # Iniial conn object created when put on queue
        # Focus on this case with "cloud:container object" arguments
        if len(args) == 2:
            obj = args[1]  # This should be object name
                       # This vvvvvvv should be cloud:container name -- must be separated when pulled from queue?
            object_queue.put((args[0], obj, options.out_file))
        else:
            for obj in args[1:]:
                object_queue.put((args[0], obj))
    while not container_queue.empty():
        sleep(0.01)
    for thread in container_threads:
        thread.abort = True
        while thread.isAlive():
            thread.join(0.01)
    put_errors_from_threads(container_threads, error_queue)
    while not object_queue.empty():
        sleep(0.01)
    for thread in object_threads:
        thread.abort = True
        while thread.isAlive():
            thread.join(0.01)
    put_errors_from_threads(object_threads, error_queue)


st_list_help = '''
list [options] [container]
    Lists the containers for the account or the objects for a container. -p or
    --prefix is an option that will only list items beginning with that prefix.
    -d or --delimiter is option (for container listings only) that will roll up
    items with the given delimiter (see Cloud Files general documentation for
    what this means).
'''.strip('\n')


def st_list(parser, args, print_queue, error_queue):
    parser.add_option('-p', '--prefix', dest='prefix', help='Will only list '
        'items beginning with the prefix')
    parser.add_option('-d', '--delimiter', dest='delimiter', help='Will roll '
        'up items with the given delimiter (see Cloud Files general '
        'documentation for what this means)')
    (options, args) = parse_args(parser, args)
    args = args[1:]
    if options.delimiter and not args:
        exit('-d option only allowed for container listings')
    if len(args) > 1:
        error_queue.put('Usage: %s [options] %s' %
                        (basename(argv[0]), st_list_help))
        return

    target_cloud = target_container = ''
    # if args, if got :, then split
    if args:
        split_args = args[0].split(':')
        if len(split_args) == 1:
            target_cloud = 'LocalCloud'
            target_container = split_args[0]
        elif len(split_args) == 2:
            target_cloud = split_args[0]
            target_container = split_args[1]
        else:
            error_queue.put('Malformed cloudname:containername %s' % args[0] )
            return
    if options.verbose > 1 :
        print "st_list: args options: ", args, options
        print "st_list: cloud:", target_cloud, " container:", target_container
    conn = get_conn(options)
    #  target_cloud is set in conn object before get_auth() called
    conn.target_cloud = target_cloud
    try:
        if target_cloud == '' and target_container =='':
            marker = ''
            print_name_once = True
            while True:
                items = conn.get_account(marker=marker, prefix=options.prefix)[1]
                if not items:
                    break
                #  We don't know if this is a vo login until the first call is done
                if  conn.vo_access == None:
                    for item in items:
                        print_queue.put(item.get('name', item.get('subdir')))
                else:
                    if  print_name_once:
                        print_queue.put('Cloud Name: %s' % conn.vo_access['vo_self_name'])
                        print_name_once = False
                    for item in items:
                        print_queue.put( "%s:  Permissions: %s  File Count: %s  Total Bytes: %s" %
                                         (item.get('name', item.get('subdir')),
                                          item.get('permissions', item.get('subdir')),
                                          item.get('count', item.get('subdir')),
                                          item.get('bytes', item.get('subdir'))
                                         )
                                       )
                marker = items[-1].get('name', items[-1].get('subdir'))
            #  If this was a vo login, now do all the other vo sites
            if  conn.vo_access != None:
                save_url = conn.url
                save_token = conn.token
                for vo_site in conn.vo_access['vo_others']:
                    print_queue.put('Cloud Name: %s' % vo_site['cloud_name'])
                    conn.url = vo_site['endpoint']
                    conn.token = vo_site['auth_token']
                    conn.http_conn = None
                    marker = ''
                    while True:
                        items = conn.get_account(marker=marker, prefix=options.prefix)[1]
                        if not items:
                            break
                        for item in items:
                            print_queue.put( "%s:  Permissions: %s  File Count: %s  Total Bytes: %s" %
                                             (item.get('name', item.get('subdir')),
                                              item.get('permissions', item.get('subdir')),
                                              item.get('count', item.get('subdir')),
                                              item.get('bytes', item.get('subdir'))
                                             )
                                           )
                        marker = items[-1].get('name', items[-1].get('subdir'))
                conn.url = save_url
                conn.token = save_token

        elif target_cloud != '' and target_container == '':
            print_queue.put('Cloud Name: %s' % target_cloud)
            marker = ''
            while True:
                items = conn.get_account(marker=marker, prefix=options.prefix)[1]
                if not items:
                    break
                for item in items:
                    print_queue.put( "%s:  Permissions: %s  File Count: %s  Total Bytes: %s" %
                                     (item.get('name', item.get('subdir')),
                                      item.get('permissions', item.get('subdir')),
                                      item.get('count', item.get('subdir')),
                                      item.get('bytes', item.get('subdir'))
                                      )
                                     )
                marker = items[-1].get('name', items[-1].get('subdir'))

        elif target_cloud == '' and target_container != '':
            error_queue.put('Unsupported case :containername %s' % args[0] )

        else:  # target_cloud != '' and target_container != ''
            print_queue.put('Cloud Name: %s' % target_cloud)
            marker = ''
            while True:
                items = conn.get_container(target_container, marker=marker,
                                           prefix=options.prefix, delimiter=options.delimiter)[1]
                if not items:
                    break
                for item in items:
                    print_queue.put(item.get('name', item.get('subdir')))
                marker = items[-1].get('name', items[-1].get('subdir'))

    except ClientException, err:
        if err.http_status != 404:
            raise
        if not args:
            error_queue.put('Account not found')
        else:
            error_queue.put('Container %s not found' % repr(target_container))


st_stat_help = '''
stat [container] [object]
    Displays information for the account, container, or object depending on the
    args given (if any).'''.strip('\n')


def st_stat(parser, args, print_queue, error_queue):
    (options, args) = parse_args(parser, args)
    #print "options:", options, "args:", args
    args = args[1:]
    conn = get_conn(options)
    # For now this is sufficient; when specific containers can be stat'ed then
    # target_cloud and target_container will have to be set as in st_list
    conn.target_cloud = ''
    #print "url after get_conn:", conn.url
    #print "toekn after get_conn:", conn.token
    if not args:
        try:
            headers = conn.head_account()
	    #print "conn vo_access:", conn.vo_access
            if options.verbose > 1:
                print_queue.put('''
StorageURL: %s
Auth Token: %s
'''.strip('\n') % (conn.url, conn.token))
            container_count = int(headers.get('x-account-container-count', 0))
            object_count = int(headers.get('x-account-object-count', 0))
            bytes_used = int(headers.get('x-account-bytes-used', 0))
            print_queue.put('''
   Account: %s
Containers: %d
   Objects: %d
     Bytes: %d'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], container_count,
                                 object_count, bytes_used))
            for key, value in headers.items():
                if key.startswith('x-account-meta-'):
                    print_queue.put('%10s: %s' % ('Meta %s' %
                        key[len('x-account-meta-'):].title(), value))
            for key, value in headers.items():
                if not key.startswith('x-account-meta-') and key not in (
                        'content-length', 'date', 'x-account-container-count',
                        'x-account-object-count', 'x-account-bytes-used'):
                    print_queue.put(
                        '%10s: %s' % (key.title(), value))
        except ClientException, err:
            if err.http_status != 404:
                raise
            error_queue.put('Account not found')
    elif len(args) == 1:
        if '/' in args[0]:
            print >> stderr, 'WARNING: / in container name; you might have ' \
                             'meant %r instead of %r.' % \
                             (args[0].replace('/', ' ', 1), args[0])
        try:
            headers = conn.head_container(args[0])
            if options.verbose > 1:
                print "conn vo_access:", conn.vo_access

            if conn.vo_access == None:
                print_container_header( conn, None, args[0], headers )
            else:
                print_container_header( conn, conn.vo_access['vo_self_name'], args[0], headers )
                for vo_site in conn.vo_access['vo_others']:
                    conn.url = vo_site['endpoint']
                    conn.token = vo_site['auth_token']
                    conn.http_conn = None
                    vo_header = conn.head_container( args[0] )
                    #print "vo_header:", vo_header
                    print_container_header( conn, vo_site['cloud_name'], args[0], vo_header )
        except ClientException, err:
            if err.http_status != 404:
                raise
            error_queue.put('Container %s not found' % repr(args[0]))

    elif len(args) == 2:
        try:
            headers = conn.head_object(args[0], args[1])
            print_queue.put('''
       Account: %s
     Container: %s
        Object: %s
  Content Type: %s'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], args[0],
                                     args[1], headers.get('content-type')))
            if 'content-length' in headers:
                print_queue.put('Content Length: %s' %
                                headers['content-length'])
            if 'last-modified' in headers:
                print_queue.put(' Last Modified: %s' %
                                headers['last-modified'])
            if 'etag' in headers:
                print_queue.put('          ETag: %s' % headers['etag'])
            if 'x-object-manifest' in headers:
                print_queue.put('      Manifest: %s' %
                                headers['x-object-manifest'])
            for key, value in headers.items():
                if key.startswith('x-object-meta-'):
                    print_queue.put('%14s: %s' % ('Meta %s' %
                        key[len('x-object-meta-'):].title(), value))
            for key, value in headers.items():
                if not key.startswith('x-object-meta-') and key not in (
                        'content-type', 'content-length', 'last-modified',
                        'etag', 'date', 'x-object-manifest'):
                    print_queue.put(
                        '%14s: %s' % (key.title(), value))
        except ClientException, err:
            if err.http_status != 404:
                raise
            error_queue.put('Object %s not found' %
                            repr('%s/%s' % (args[0], args[1])))
    else:
        error_queue.put('Usage: %s [options] %s' %
                        (basename(argv[0]), st_stat_help))


def print_container_header( conn, cloudname, containername, headers):
    if cloudname == None:
        cloudcontainername = containername
    else:
        cloudcontainername = cloudname + ':' + containername
    object_count = int(headers.get('x-container-object-count', 0))
    bytes_used = int(headers.get('x-container-bytes-used', 0))
    print_queue.put('''
===============================
  Account: %s
Container: %s
  Objects: %d
    Bytes: %d
 Read ACL: %s
Write ACL: %s
  Sync To: %s
 Sync Key: %s'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], cloudcontainername,
                                object_count, bytes_used,
                                headers.get('x-container-read', ''),
                                headers.get('x-container-write', ''),
                                headers.get('x-container-sync-to', ''),
                                headers.get('x-container-sync-key', '')))
    for key, value in headers.items():
        if key.startswith('x-container-meta-'):
            print_queue.put('%9s: %s' % ('Meta %s' %
                                         key[len('x-container-meta-'):].title(), value))
    for key, value in headers.items():
        if not key.startswith('x-container-meta-') and key not in (
            'content-length', 'date', 'x-container-object-count',
            'x-container-bytes-used', 'x-container-read',
            'x-container-write', 'x-container-sync-to',
            'x-container-sync-key'):
            print_queue.put('%9s: %s' % (key.title(), value))


st_post_help = '''
post [options] [container] [object]
    Updates meta information for the account, container, or object depending on
    the args given. If the container is not found, it will be created
    automatically; but this is not true for accounts and objects. Containers
    also allow the -r (or --read-acl) and -w (or --write-acl) options. The -m
    or --meta option is allowed on all and used to define the user meta data
    items to set in the form Name:Value. This option can be repeated. Example:
    post -m Color:Blue -m Size:Large'''.strip('\n')


def st_post(parser, args, print_queue, error_queue):
    parser.add_option('-r', '--read-acl', dest='read_acl', help='Sets the '
        'Read ACL for containers. Quick summary of ACL syntax: .r:*, '
        '.r:-.example.com, .r:www.example.com, account1, account2:user2')
    parser.add_option('-w', '--write-acl', dest='write_acl', help='Sets the '
        'Write ACL for containers. Quick summary of ACL syntax: account1, '
        'account2:user2')
    parser.add_option('-t', '--sync-to', dest='sync_to', help='Sets the '
        'Sync To for containers, for multi-cluster replication.')
    parser.add_option('-k', '--sync-key', dest='sync_key', help='Sets the '
        'Sync Key for containers, for multi-cluster replication.')
    parser.add_option('-m', '--meta', action='append', dest='meta', default=[],
        help='Sets a meta data item with the syntax name:value. This option '
        'may be repeated. Example: -m Color:Blue -m Size:Large')
    (options, args) = parse_args(parser, args)
    args = args[1:]
    if (options.read_acl or options.write_acl or options.sync_to or
        options.sync_key) and not args:
        exit('-r, -w, -t, and -k options only allowed for containers')
    conn = get_conn(options)
    # A post should always be just to the locally administered containers
    conn.target_cloud = ''
    if not args:
        headers = split_headers(options.meta, 'X-Account-Meta-', error_queue)
        try:
            conn.post_account(headers=headers)
        except ClientException, err:
            if err.http_status != 404:
                raise
            error_queue.put('Account not found')
    elif len(args) == 1:
        if '/' in args[0]:
            print >> stderr, 'WARNING: / in container name; you might have ' \
                             'meant %r instead of %r.' % \
                             (args[0].replace('/', ' ', 1), args[0])
        headers = split_headers(options.meta, 'X-Container-Meta-', error_queue)
        if options.read_acl is not None:
            headers['X-Container-Read'] = options.read_acl
        if options.write_acl is not None:
            headers['X-Container-Write'] = options.write_acl
        if options.sync_to is not None:
            headers['X-Container-Sync-To'] = options.sync_to
        if options.sync_key is not None:
            headers['X-Container-Sync-Key'] = options.sync_key
        try:
            conn.post_container(args[0], headers=headers)
        except ClientException, err:
            if err.http_status != 404:
                raise
            conn.put_container(args[0], headers=headers)
    elif len(args) == 2:
        headers = split_headers(options.meta, 'X-Object-Meta-', error_queue)
        try:
            conn.post_object(args[0], args[1], headers=headers)
        except ClientException, err:
            if err.http_status != 404:
                raise
            error_queue.put('Object %s not found' %
                            repr('%s/%s' % (args[0], args[1])))
    else:
        error_queue.put('Usage: %s [options] %s' %
                        (basename(argv[0]), st_post_help))


st_upload_help = '''
upload [options] container file_or_directory [file_or_directory] [...]
    Uploads to the given container the files and directories specified by the
    remaining args. -c or --changed is an option that will only upload files
    that have changed since the last upload. -S <size> or --segment-size <size>
    and --leave-segments are options as well (see --help for more).
'''.strip('\n')


def st_upload(parser, args, print_queue, error_queue):
    parser.add_option('-c', '--changed', action='store_true', dest='changed',
        default=False, help='Will only upload files that have changed since '
        'the last upload')
    parser.add_option('-S', '--segment-size', dest='segment_size', help='Will '
        'upload files in segments no larger than <size> and then create a '
        '"manifest" file that will download all the segments as if it were '
        'the original file. The segments will be uploaded to a '
        '<container>_segments container so as to not pollute the main '
        '<container> listings.')
    parser.add_option('', '--leave-segments', action='store_true',
        dest='leave_segments', default=False, help='Indicates that you want '
        'the older segments of manifest objects left alone (in the case of '
        'overwrites)')
    parser.add_option('', '--object-threads', type=int,
                      default=10, help='Number of threads to use for '
                      'uploading full objects')
    parser.add_option('', '--segment-threads', type=int,
                      default=10, help='Number of threads to use for '
                      'uploading object segments')
    (options, args) = parse_args(parser, args)
    args = args[1:]
    if len(args) < 2:
        error_queue.put('Usage: %s [options] %s' %
                        (basename(argv[0]), st_upload_help))
        return
    object_queue = Queue(10000)

    def _segment_job(job, conn):
        if job.get('delete', False):
            conn.delete_object(job['container'], job['obj'])
        else:
            fp = open(job['path'], 'rb')
            fp.seek(job['segment_start'])
            conn.put_object(job.get('container', args[0] + '_segments'),
                job['obj'], fp, content_length=job['segment_size'])
        if options.verbose and 'log_line' in job:
            if conn.attempts > 1:
                print_queue.put('%s [after %d attempts]' %
                                (job['log_line'], conn.attempts))
            else:
                print_queue.put(job['log_line'])

    def _object_job(job, conn):
        path = job['path']
        container = job.get('container', args[0])
        dir_marker = job.get('dir_marker', False)
        #print "_object_job entry: ", args[0], container, path, dir_marker
        #
        # If "container" has :, then split into target_cloud and container
        #
        split_args = container.split(':')
        if len(split_args) == 1:
            conn.target_cloud = 'LocalCloud'
            # 'container' is just the container name
        elif len(split_args) == 2:
            conn.target_cloud = split_args[0]
            container = split_args[1]
        else:
            error_queue.put('Malformed cloudname:containername %s' % container )
            return

        #print "_object_job: ", conn.target_cloud, container, path, dir_marker

        try:
            obj = path
            if obj.startswith('./') or obj.startswith('.\\'):
                obj = obj[2:]
            if obj.startswith('/'):
                obj = obj[1:]
            put_headers = {'x-object-meta-mtime': str(getmtime(path))}
            if dir_marker:
                if options.changed:
                    try:
                        headers = conn.head_object(container, obj)
                        ct = headers.get('content-type')
                        cl = int(headers.get('content-length'))
                        et = headers.get('etag')
                        mt = headers.get('x-object-meta-mtime')
                        if ct.split(';', 1)[0] == 'text/directory' and \
                                cl == 0 and \
                                et == 'd41d8cd98f00b204e9800998ecf8427e' and \
                                mt == put_headers['x-object-meta-mtime']:
                            return
                    except ClientException, err:
                        if err.http_status != 404:
                            raise
                conn.put_object(container, obj, '', content_length=0,
                                content_type='text/directory',
                                headers=put_headers)
            else:
                # We need to HEAD all objects now in case we're overwriting a
                # manifest object and need to delete the old segments
                # ourselves.
                old_manifest = None
                if options.changed or not options.leave_segments:
                    try:
                        headers = conn.head_object(container, obj)
                        cl = int(headers.get('content-length'))
                        mt = headers.get('x-object-meta-mtime')
                        if options.changed and cl == getsize(path) and \
                                mt == put_headers['x-object-meta-mtime']:
                            return
                        if not options.leave_segments:
                            old_manifest = headers.get('x-object-manifest')
                    except ClientException, err:
                        if err.http_status != 404:
                            raise
                if options.segment_size and \
                        getsize(path) < options.segment_size:
                    full_size = getsize(path)
                    segment_queue = Queue(10000)
                    segment_threads = [QueueFunctionThread(segment_queue,
                        _segment_job, create_connection()) for _junk in
                        xrange(options.segment_threads)]
                    for thread in segment_threads:
                        thread.start()
                    segment = 0
                    segment_start = 0
                    while segment_start < full_size:
                        segment_size = int(options.segment_size)
                        if segment_start + segment_size > full_size:
                            segment_size = full_size - segment_start
                        segment_queue.put({'path': path,
                            'obj': '%s/%s/%s/%08d' % (obj,
                                put_headers['x-object-meta-mtime'], full_size,
                                segment),
                            'segment_start': segment_start,
                            'segment_size': segment_size,
                            'log_line': '%s segment %s' % (obj, segment)})
                        segment += 1
                        segment_start += segment_size
                    while not segment_queue.empty():
                        sleep(0.01)
                    for thread in segment_threads:
                        thread.abort = True
                        while thread.isAlive():
                            thread.join(0.01)
                    if put_errors_from_threads(segment_threads, error_queue):
                        raise ClientException('Aborting manifest creation '
                            'because not all segments could be uploaded. %s/%s'
                            % (container, obj))
                    new_object_manifest = '%s_segments/%s/%s/%s/' % (
                        quote(container), quote(obj),
                        put_headers['x-object-meta-mtime'], full_size)
                    if old_manifest == new_object_manifest:
                        old_manifest = None
                    put_headers['x-object-manifest'] = new_object_manifest
                    conn.put_object(container, obj, '', content_length=0,
                                    headers=put_headers)
                else:
                    conn.put_object(container, obj, open(path, 'rb'),
                        content_length=getsize(path), headers=put_headers)
                if old_manifest:
                    segment_queue = Queue(10000)
                    scontainer, sprefix = old_manifest.split('/', 1)
                    scontainer = unquote(scontainer)
                    sprefix = unquote(sprefix)
                    for delobj in conn.get_container(scontainer,
                                                     prefix=sprefix)[1]:
                        segment_queue.put({'delete': True,
                            'container': scontainer, 'obj': delobj['name']})
                    if not segment_queue.empty():
                        segment_threads = [QueueFunctionThread(segment_queue,
                            _segment_job, create_connection()) for _junk in
                            xrange(options.segment_threads)]
                        for thread in segment_threads:
                            thread.start()
                        while not segment_queue.empty():
                            sleep(0.01)
                        for thread in segment_threads:
                            thread.abort = True
                            while thread.isAlive():
                                thread.join(0.01)
                        put_errors_from_threads(segment_threads, error_queue)
            if options.verbose:
                if conn.attempts > 1:
                    print_queue.put(
                        '%s [after %d attempts]' % (obj, conn.attempts))
                else:
                    print_queue.put(obj)
        except OSError, err:
            if err.errno != ENOENT:
                raise
            error_queue.put('Local file %s not found' % repr(path))

    def _upload_dir(path):
        names = listdir(path)
        if not names:
            object_queue.put({'path': path, 'dir_marker': True})
        else:
            for name in listdir(path):
                subpath = join(path, name)
                if isdir(subpath):
                    _upload_dir(subpath)
                else:
                    object_queue.put({'path': subpath})

    # Routine body starts here

    create_connection = lambda: get_conn(options)
    object_threads = [QueueFunctionThread(object_queue, _object_job, create_connection())
                      for _junk in xrange(options.object_threads)]
    for thread in object_threads:
        thread.start()
    conn = create_connection()
    #
    # If args[0] has :, then split into target_cloud and container_name
    # Leave args[0] unchanged
    #
    split_args = args[0].split(':')
    if len(split_args) == 1:
        conn.target_cloud = 'LocalCloud'
        container_name = split_args[0]
    elif len(split_args) == 2:
        conn.target_cloud = split_args[0]
        container_name = split_args[1]
    else:
        error_queue.put('Malformed cloudname:containername %s' % args[0] )
        return
    #print "st_upload: ", conn.target_cloud, args

    # Try to create the container, just in case it doesn't exist. If this
    # fails, it might just be because the user doesn't have container PUT
    # permissions, so we'll ignore any error. If there's really a problem,
    # it'll surface on the first object PUT.
    # args[0] could be cloud:container arg

    try:
        conn.put_container(container_name)
        if options.segment_size is not None:
            conn.put_container(args[0] + '_segments')
    except ClientException, err:
        msg = ' '.join(str(x) for x in (err.http_status, err.http_reason))
        if err.http_response_content:
            if msg:
                msg += ': '
            msg += err.http_response_content[:60]
        error_queue.put(
            'Error trying to create container %r: %s' % (container_name, msg))
    except Exception, err:
        error_queue.put(
            'Error trying to create container %r: %s' % (container_name, err))
    try:
        for arg in args[1:]:
            if isdir(arg):
                _upload_dir(arg)
            else:
                object_queue.put({'path': arg})
        while not object_queue.empty():
            sleep(0.01)
        for thread in object_threads:
            thread.abort = True
            while thread.isAlive():
                thread.join(0.01)
        put_errors_from_threads(object_threads, error_queue)
    except ClientException, err:
        if err.http_status != 404:
            raise
        error_queue.put('Account not found')


def split_headers(options, prefix='', error_queue=None):
    """
    Splits 'Key: Value' strings and returns them as a dictionary.

    :param options: An array of 'Key: Value' strings
    :param prefix: String to prepend to all of the keys in the dictionary.
    :param error_queue: Queue for thread safe error reporting.
    """
    headers = {}
    for item in options:
        split_item = item.split(':', 1)
        if len(split_item) == 2:
            headers[prefix + split_item[0]] = split_item[1]
        else:
            error_string = "Metadata parameter %s must contain a ':'.\n%s" \
                           % (item, st_post_help)
            if error_queue:
                error_queue.put(error_string)
            else:
                exit(error_string)
    return headers


def parse_args(parser, args, enforce_requires=True):
    if not args:
        args = ['-h']
    (options, args) = parser.parse_args(args)

    if (not (options.auth and options.user and options.key) or
        options.os_auth_url):
        # Use 2.0 auth if none of the old args are present
        options.auth_version = '2.0'

    # Use new-style args if old ones not present
    if not options.auth and options.os_auth_url:
        options.auth = options.os_auth_url
    if not options.user and options.os_username:
        options.user = options.os_username
    if not options.key and options.os_password:
        options.key = options.os_password

    # Specific OpenStack options
    options.os_options = {
        'tenant_id': options.os_tenant_id,
        'tenant_name': options.os_tenant_name,
        'service_type': options.os_service_type,
        'endpoint_type': options.os_endpoint_type,
        'auth_token': options.os_auth_token,
        'object_storage_url': options.os_storage_url,
        'region_name': options.os_region_name,
    }

    # Handle trailing '/' in URL
    if options.auth and not options.auth.endswith('/'):
        options.auth += '/'

    if enforce_requires and \
            not (options.auth and options.user and options.key):
        exit('''
Auth version 1.0 requires ST_AUTH, ST_USER, and ST_KEY environment variables
to be set or overridden with -A, -U, or -K.

Auth version 2.0 requires OS_AUTH_URL, OS_USERNAME, OS_PASSWORD, and
OS_TENANT_NAME OS_TENANT_ID to be set or overridden with --os-auth-url,
--os-username, --os-password, --os-tenant-name or os-tenant-id.'''.strip('\n'))
    return options, args


if __name__ == '__main__':
    parser = OptionParser(version='%prog 1.0', usage='''
Usage: %%prog <command> [options] [args]

Commands:
  %(st_stat_help)s
  %(st_list_help)s
  %(st_upload_help)s
  %(st_post_help)s
  %(st_download_help)s
  %(st_delete_help)s

Example:
  %%prog -A https://auth.api.rackspacecloud.com/v1.0 -U user -K key stat
'''.strip('\n') % globals())
    parser.add_option('-s', '--snet', action='store_true', dest='snet',
                      default=False, help='Use SERVICENET internal network')
    parser.add_option('-v', '--verbose', action='count', dest='verbose',
                      default=1, help='Print more info')
    parser.add_option('-q', '--quiet', action='store_const', dest='verbose',
                      const=0, default=1, help='Suppress status output')
    parser.add_option('-A', '--auth', dest='auth',
                      default=environ.get('ST_AUTH'),
                      help='URL for obtaining an auth token')
    parser.add_option('-V', '--auth-version',
                      dest='auth_version',
                      default=environ.get('ST_AUTH_VERSION', '1.0'),
                      type=str,
                      help='Specify a version for authentication'
                           '(default: 1.0)')
    parser.add_option('-U', '--user', dest='user',
                      default=environ.get('ST_USER'),
                      help='User name for obtaining an auth token')
    parser.add_option('-K', '--key', dest='key',
                      default=environ.get('ST_KEY'),
                      help='Key for obtaining an auth token')
    parser.add_option('--os-username',
                      metavar='<auth-user-name>',
                      default=environ.get('OS_USERNAME'),
                      help='Openstack username. Defaults to env[OS_USERNAME].')
    parser.add_option('--os_username',
                      help=SUPPRESS_HELP)
    parser.add_option('--os-password',
                      metavar='<auth-password>',
                      default=environ.get('OS_PASSWORD'),
                      help='Openstack password. Defaults to env[OS_PASSWORD].')
    parser.add_option('--os_password',
                      help=SUPPRESS_HELP)
    parser.add_option('--os-tenant-id',
                      metavar='<auth-tenant-id>',
                      default=environ.get('OS_TENANT_ID'),
                      help='OpenStack tenant ID.'
                           'Defaults to env[OS_TENANT_ID]')
    parser.add_option('--os_tenant_id',
                      help=SUPPRESS_HELP)
    parser.add_option('--os-tenant-name',
                      metavar='<auth-tenant-name>',
                      default=environ.get('OS_TENANT_NAME'),
                      help='Openstack tenant name.'
                           'Defaults to env[OS_TENANT_NAME].')
    parser.add_option('--os_tenant_name',
                      help=SUPPRESS_HELP)
    parser.add_option('--os-auth-url',
                      metavar='<auth-url>',
                      default=environ.get('OS_AUTH_URL'),
                      help='Openstack auth URL. Defaults to env[OS_AUTH_URL].')
    parser.add_option('--os_auth_url',
                      help=SUPPRESS_HELP)
    parser.add_option('--os-auth-token',
                      metavar='<auth-token>',
                      default=environ.get('OS_AUTH_TOKEN'),
                      help='Openstack token. Defaults to env[OS_AUTH_TOKEN]')
    parser.add_option('--os_auth_token',
                      help=SUPPRESS_HELP)
    parser.add_option('--os-storage-url',
                      metavar='<storage-url>',
                      default=environ.get('OS_STORAGE_URL'),
                      help='Openstack storage URL.'
                           'Defaults to env[OS_STORAGE_URL]')
    parser.add_option('--os_storage_url',
                      help=SUPPRESS_HELP)
    parser.add_option('--os-region-name',
                      metavar='<region-name>',
                      default=environ.get('OS_REGION_NAME'),
                      help='Openstack region name. '
                           'Defaults to env[OS_REGION_NAME]')
    parser.add_option('--os_region_name',
                      help=SUPPRESS_HELP)
    parser.add_option('--os-service-type',
                      metavar='<service-type>',
                      default=environ.get('OS_SERVICE_TYPE'),
                      help='Openstack Service type.'
                           'Defaults to env[OS_SERVICE_TYPE]')
    parser.add_option('--os_service_type',
                      help=SUPPRESS_HELP)
    parser.add_option('--os-endpoint-type',
                      metavar='<endpoint-type>',
                      default=environ.get('OS_ENDPOINT_TYPE'),
                      help='Openstack Endpoint type. (publicURL, e.g.)' \
                           'Defaults to env[OS_ENDPOINT_TYPE]')
    parser.disable_interspersed_args()
    (options, args) = parse_args(parser, argv[1:], enforce_requires=False)
    parser.enable_interspersed_args()

    commands = ('delete', 'download', 'list', 'post', 'stat', 'upload')
    if not args or args[0] not in commands:
        parser.print_usage()
        if args:
            exit('no such command: %s' % args[0])
        exit()

    signal.signal(signal.SIGINT, attempt_graceful_exit)

    print_queue = Queue(10000)

    def _print(item):
        if isinstance(item, unicode):
            item = item.encode('utf8')
        print item

    print_thread = QueueFunctionThread(print_queue, _print)
    print_thread.start()

    error_count = 0
    error_queue = Queue(10000)

    def _error(item):
        global error_count
        error_count += 1
        if isinstance(item, unicode):
            item = item.encode('utf8')
        print >> stderr, item

    error_thread = QueueFunctionThread(error_queue, _error)
    error_thread.start()

    try:
        parser.usage = globals()['st_%s_help' % args[0]]
        try:
            globals()['st_%s' % args[0]](parser, argv[1:], print_queue,
                                         error_queue)
        except (ClientException, HTTPException, socket.error), err:
            error_queue.put(str(err))
        while not print_queue.empty():
            sleep(0.01)
        print_thread.abort = True
        while print_thread.isAlive():
            print_thread.join(0.01)
        while not error_queue.empty():
            sleep(0.01)
        error_thread.abort = True
        while error_thread.isAlive():
            error_thread.join(0.01)
        if error_count:
            exit(1)
    except (SystemExit, Exception):
        for thread in threading_enumerate():
            thread.abort = True
        raise
